package com.mytx.live.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.mytx.common.annotation.SerializedField;
import com.mytx.common.constant.Constant;
import com.mytx.common.pojo.CommonResult;
import com.mytx.common.pojo.RedisLock;
import com.mytx.common.utils.DateUtils;
import com.mytx.common.utils.EntityUtils;
import com.mytx.common.utils.IDUtils;
import com.mytx.live.service.LiveService;
import com.mytx.live.utils.PayUtils;
import com.mytx.mapper.*;
import com.mytx.pojo.*;
import jdk.nashorn.internal.objects.NativeUint8Array;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings({"SpringJavaInjectionPointsAutowiringInspection", "Duplicates"})
@RestController
public class AdvertisementController {
    private static Logger logger = LoggerFactory.getLogger(AdvertisementController.class);


    @Autowired
    UserMapper userMapper;

    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    HttpServletRequest request;

    @Autowired
    OrdersMapper ordersMapper;

    @Autowired
    AdvertisementMapper advertisementMapper;

    @Autowired
    MyFileMapper myFileMapper;

    @Autowired
    DataSourceTransactionManager transactionManager;


    @SerializedField(encrypt = false)
    @RequestMapping(value = "/queryAdvertisement", method = RequestMethod.POST)
    public CommonResult queryAdvertisement(@RequestBody String advertisementId) {
        logger.debug("queryAdvertisement:advertisementId:{}", advertisementId);
        Object advertisementObject = redisTemplate.opsForHash().entries(Constant.ADVERTISEMENT_RECORD + advertisementId);
        if (!((Map) advertisementObject).isEmpty()) {
            return CommonResult.ok(advertisementObject);
        } else {
            Advertisement advertisement = advertisementMapper.selectByPrimaryKey(advertisementId);
            if (advertisement != null) {
                //缓存一份进redis
                redisTemplate.opsForHash().putAll(Constant.ADVERTISEMENT_RECORD + advertisementId, EntityUtils.objectToMap_v1(advertisement));
                //1天后自动过期，目前设计的广告位长度是1天，所以一天内到期足够了
                redisTemplate.expire(Constant.ADVERTISEMENT_RECORD + advertisementId, 1, TimeUnit.DAYS);
            }
            return CommonResult.ok(advertisement);
        }

    }

    /**
     * 投放广告位
     * 手动控制事务，不能用 注解（@Transactional）的形式
     *
     * @return
     */
    @PostMapping("/putAdvertisement")
    public CommonResult putAdvertisement(@RequestBody JSONObject body) {
        String uId = request.getHeader("uId");
        Advertisement advertisement = body.getObject("advertisement", Advertisement.class);
        String payPassword = body.getString("payPassword");
        User user = getUser(uId);
        //1.获取事务定义
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        //2.设置事务隔离级别，开启新事务
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        //3.获得事务状态
        TransactionStatus transactionStatus = transactionManager.getTransaction(def);
        RedisLock redisLock = new RedisLock(redisTemplate, Constant.USER_SESSION_LOCK + uId);
        try {
            if (!redisLock.lock()) {
                return CommonResult.build(Constant.BALANCE_HAD_LOCK, "提交广告位没有获取到锁");
            }
            String liveRoomId = advertisement.getrId();
            Date currentDate = new Date();
            //当前广告位的单价
            BigDecimal amount = advertisement.getAmount();
            Date startDate = advertisement.getStartDate();
            Date endDate = advertisement.getEndDate();
            //检查出价和时间段是否合理
            CommonResult x = checkOffer(liveRoomId, currentDate, amount, startDate, endDate);
            if (x != null) return x;
            String advertisementId;
            BigDecimal deposit = null;//当前出价所需保证金
            BigDecimal incrementDeposit = null;//增加的保证金
            BigDecimal reduceDeposit = null;//减少的保证金
            BigDecimal historyDeposit = null;//历史留存保证金
            BigDecimal mysqlDeposit = null;//用户账面保证金
            int offerCount = 1;
            Advertisement historyAdvertisement = advertisementMapper.selectMyValidAdvertisement(advertisement.getrId(), uId);
            deposit = advertisement.getDeposit();
            if (historyAdvertisement == null) {
                advertisementId = IDUtils.genOrderItemId();
                //判断余额是否支付保证金
                CommonResult commonResult = PayUtils.payPrepare(payPassword, deposit, user);
                if (commonResult != null) {
                    return commonResult;
                }
                advertisement.setId(advertisementId);
                advertisement.setuId(uId);
                advertisement.setOfferCount(offerCount);
                advertisement.setStatus(Constant.ADVERTISEMENT_STATUS_INIT);
                advertisement.setCreated(currentDate);
                advertisementMapper.insertSelective(advertisement);
                mysqlDeposit = userMapper.incrementDeposit(uId, deposit);


            } else {
                //如果当前直播间有没到期的广告位，说明这次是用户增加单价，保证金只需要补齐差额
                //如果过期时间不同，删除旧的过期时间，以新出价的过期时间为准
                advertisementId = historyAdvertisement.getId();
                offerCount = historyAdvertisement.getOfferCount() + 1;
                historyDeposit = historyAdvertisement.getDeposit();
                Date historyEndDate = historyAdvertisement.getEndDate();

                //如果当前的广告保证金大于历史保证金，需要补交保证金
                if (deposit.compareTo(historyDeposit) > 0) {
                    incrementDeposit = deposit.subtract(historyDeposit);
                    //判断余额是否支付保证金
                    CommonResult commonResult = PayUtils.payPrepare(payPassword, incrementDeposit, user);
                    if (commonResult != null) {
                        return commonResult;
                    }
                    mysqlDeposit = userMapper.incrementDeposit(uId, incrementDeposit);
                } else if (deposit.compareTo(historyDeposit) < 0) {//当前保证金小于历史保证金，需要减少保证金

                    reduceDeposit = historyDeposit.subtract(deposit);

                    mysqlDeposit = userMapper.reduceDeposit(uId, reduceDeposit);
                } else {
                    //当前保证金等于之前的保证金，无须补交保证金，以之前投放的广告保证金为准
                    advertisement.setDeposit(null);
                }
                advertisement.setUpdated(currentDate);
                advertisement.setId(advertisementId);
                advertisement.setOfferCount(offerCount);
                advertisementMapper.updateByPrimaryKeySelective(advertisement);

                //如果二次提交的广告结束时间和前一次提交的广告结束时间不一致，应该删除前一次提交的广告位结束时间在redis 中的到期结算记录
                if (!endDate.equals(historyEndDate)) {
                    String formatDate = DateUtils.formatDate(historyEndDate, Constant.DATE_HOUR_FORMAT);
                    String expireKey = Constant.ADVERTISEMENT_EXPIRE_SET + formatDate;
                    redisTemplate.opsForSet().remove(expireKey, advertisementId);
                    //以当前提交的广告结束时间 在redis 中生成到期结算记录
                    saveInExpireSet(advertisementId, endDate);
                }
            }

            if (historyAdvertisement == null) {
                logger.info("putAdvertisement:首次出价，保证金增加 uId:{},advertisementId:{},amount:{},deposit:{},mysqlDeposit:{},startDate:{},endDate:{}",
                        uId, advertisementId, amount, deposit, mysqlDeposit, startDate, endDate);
            } else if (incrementDeposit != null) {
                logger.info("putAdvertisement:第{}次出价，保证金不足，补齐保证金 uId:{},,advertisementId:{},单价 amount:{},补交的保证金 incrementDeposit:{},当前投放所需保证金:{},历史出价留存保证金:{},mysqlDeposit:{},startDate:{},endDate:{}",
                        offerCount, uId, advertisementId, amount, incrementDeposit, deposit, historyDeposit, mysqlDeposit, startDate, endDate);
            } else if (reduceDeposit != null) {

                logger.info("putAdvertisement:第{}次出价，保证金减少 uId:{},advertisementId:{},单价 amount:{}, 减少的保证金:{},当前投放所需保证金:{},历史出价留存保证金:{},mysqlDeposit:{},startDate:{},endDate:{}",
                        offerCount, uId, advertisementId, amount, reduceDeposit, deposit, historyDeposit, mysqlDeposit, startDate, endDate);
            } else {
                logger.info("putAdvertisement:第{}次出价，保证金没有变化 uId:{},advertisementId:{},单价 amount:{},当前投放所需保证金:{},历史出价留存保证金:{},startDate:{},endDate:{}",
                        offerCount, uId, advertisementId, amount, deposit, historyDeposit, startDate, endDate);
            }
            JSONObject result = new JSONObject();
            if (mysqlDeposit != null) {
                result.put("deposit", mysqlDeposit);
            }
            result.put("offerCount", offerCount);
            JSONObject info = new JSONObject();
            info.put("advertisement", advertisement);
            if (historyAdvertisement != null) {
                info.put("historyAdvertisement", historyAdvertisement);
            } else {
                //以当前提交的广告结束时间 在redis 中生成到期记录
                saveInExpireSet(advertisementId, endDate);
                //第一次出价，需要把生成的广告位id  回传给前端
                result.put("advertisementId", advertisementId);
            }
            //必须在消息队列发出之前，手动提交事务，不然有可能在消息队列中读到的mysql中的数据还没有更新
            transactionManager.commit(transactionStatus);
            //通过消息队列，生成缓存中的最高价
            redisTemplate.convertAndSend(Constant.NOTIFY_MODIFY_ADVERTISEMENT_MAX_PRICE, info);
            return CommonResult.ok(result);

        } catch (InterruptedException e) {
            //事务回滚
            transactionManager.rollback(transactionStatus);
            e.printStackTrace();
            logger.error("offerToAdvertisement_v2:出价失败:{}", e.getMessage());
            return CommonResult.build(Constant.UNKNOWN_ERROR, "未知错误");
        } finally {
            redisLock.unlock();
        }
    }

    /**
     * 投放广告位后，设置广告位展示内容
     *
     * @param advertisement
     * @return
     */
    @Transactional
    @PostMapping("/setAdvertisement")
    public CommonResult setAdvertisement(@RequestBody Advertisement advertisement) {
        String uId = request.getHeader("uId");
        MyFile myFile = advertisement.getMyFile();
        // advertisement.setMyFile(null);//清空掉，不然会保存到redis 中
        if (myFile.getType() == Constant.FILE_TYPE_VIDEO) {
            advertisement.setVideo(myFile.getUrl());
        } else {
            advertisement.setPicture(myFile.getUrl());
        }
        Date currentDate = new Date();
        if (myFile.getStatus() == Constant.FILE_STATUS_REMOTE) {

            myFile.setUpdated(currentDate);
            List<MyFile> fileList = new ArrayList<>();
            fileList.add(myFile);
            myFileMapper.updateBatch(fileList);
        } else {
            myFile.setuId(uId);
            myFile.setStatus(Constant.FILE_STATUS_REMOTE);
            myFile.setReferenceCount(1);//其实这儿也可以在 mysql 建表中设置为默认值为1
            myFile.setCreated(currentDate);
            myFileMapper.insert(myFile);
        }
        Date startDate = advertisement.getStartDate();

        if (startDate.before(currentDate) && advertisement.getUpdateCount() != null && advertisement.getUpdateCount() > 1) {
            return CommonResult.build(CommonResult.STATUS_FAIL, "过了广告开始时间，不能对展示内容进行修改");
        }
        advertisement.setUpdated(currentDate);
        advertisementMapper.setAdvertisement(advertisement);

        String liveRoomId = advertisement.getrId();
        //如果开始时间在当前时间之前，有可能用户忘记了设置广告位，需要去判断该广告位是否处于展示中，如果是，替换缓存，直接推送到前端
        if (startDate.before(currentDate)) {
            Object advertisementObject = redisTemplate.opsForHash().get(Constant.LIVE_ROOM + liveRoomId, "advertisement");
            if (advertisementObject != null) {
                Advertisement currentShowAdvertisement = ((JSONObject) advertisementObject).toJavaObject(Advertisement.class);
                if (currentShowAdvertisement.getId().equals(advertisement.getId()) && currentShowAdvertisement.getVideo() == null && currentShowAdvertisement.getPicture() == null) {
                    advertisement.setMyFile(null);//清空，不然保存进redis
                    redisTemplate.opsForHash().put(Constant.LIVE_ROOM + liveRoomId, "advertisement", advertisement);
                    //推送到前端
                    JSONObject info = new JSONObject();
                    info.put("liveRoomId", liveRoomId);
                    info.put("advertisement", advertisement);
                    redisTemplate.convertAndSend(Constant.NOTIFY_ADVERTISEMENT_CHANGE, info);

                }
            }
        }

        return CommonResult.ok();
    }


    @RequestMapping(value = "/queryOffer", method = RequestMethod.POST)
    public CommonResult queryOffer(@RequestBody Map<String, Object> info) {
        String advertisementId = info.get("advertisementId") + "";
        //Object uId = info.get("uId");
        int page = 1;
        int size = 10;
        if (info.containsKey("page")) {
            page = (int) info.get("page");
        }
        if (info.containsKey("size")) {
            size = (int) info.get("size");
        }
        PageHelper.startPage(page, size);
        List<Orders> orders = ordersMapper.queryOrderByAdvertisementId(advertisementId);
        PageInfo<Orders> offerPageInfo = new PageInfo<>(orders);
        return CommonResult.ok(offerPageInfo);
    }


    /**
     * 对广告位出价
     *
     * @return
     */
    @Transactional
    @PostMapping("/offer_v1")
    @Deprecated
    public CommonResult offerToAdvertisement(@RequestBody JSONObject body) {
        String uId = request.getHeader("uId");
        Orders orders = body.getObject("orders", Orders.class);
        String advertisementId = orders.getaId();
        BigDecimal offerAmount = orders.getAmount();
        String payPassword = body.getString("payPassword");
        User user = getUser(uId);
        CommonResult commonResult = PayUtils.payPrepare(payPassword, offerAmount, user);
        if (commonResult != null) {
            return commonResult;
        }

        Object amountObject = redisTemplate.opsForHash().get(Constant.ADVERTISEMENT_RECORD + advertisementId, "amount");
        if (amountObject == null) {
            return CommonResult.build(CommonResult.STATUS_FAIL, "广告位不存在");
        }
        BigDecimal originalAmount = (BigDecimal) amountObject;
        //判断自己对该广告位是否有历史竞价记录
        Object offerObject = redisTemplate.opsForValue().get(Constant.OFFER_RECORD + advertisementId + ":" + uId);
        Orders historyOrders = null;
        int offerCount = 1;
        BigDecimal deposit;
        String orderId;
        Date created = new Date();
        if (offerObject == null) {
            orderId = IDUtils.genOrderItemId();
            orders.setId(orderId);
            orders.setStatus(Constant.ORDER_STATUS_INIT);
            orders.setType(Constant.ORDER_TYPE_TO_ADVERTISEMENT);
            orders.setOfferCount(offerCount);//第一次出价
            orders.setCreated(created);
            deposit = offerAmount;
        } else {
            historyOrders = (Orders) offerObject;
            offerCount = historyOrders.getOfferCount() + 1;
            orderId = historyOrders.getId();
            orders.setId(orderId);
            orders.setOfferCount(offerCount);//出价次数增加1
            orders.setUpdated(created);
            deposit = offerAmount.subtract(historyOrders.getAmount());//如果不是第一次出价,保证金是两次出价的差额
        }
        RedisLock redisLock = new RedisLock(redisTemplate, Constant.ADVERTISEMENT_LOCK + advertisementId);
        try {
            if (!redisLock.lock()) {
                return CommonResult.build(Constant.OFFER_BUSY, "其他人正在出价");
            }

            //如果当前出价低于或者等于原始最高出价，返回错误信息
            if (originalAmount != null && offerAmount.compareTo(originalAmount) <= 0) {
                logger.warn("orders:originalAmount:{},offerAmount:{}", originalAmount, offerAmount);
                return CommonResult.build(Constant.OFFER_ERROR_AMOUNT_LOWER, "出价过低");
            }

            BigDecimal mysqlDeposit = userMapper.incrementDeposit(uId, deposit);
            if (historyOrders == null) {
                orders.setuId(uId);
                ordersMapper.insertSelective(orders);
                // 如果广告位的原始价格是默认值 0.10 元，说明是第一次出价，需要把该广告id 放技能一个set 中，以便在定时任务中，对这些广告位进行结算
                if (originalAmount.compareTo(new BigDecimal("0.10")) == 0) {
                    redisTemplate.opsForSet().add(Constant.ADVERTISEMENT_SET, advertisementId);
                }
                //这是一个新的offer,把uid 保存进一个set里面，标记此用户对该广告位有过出价，
                // 方便在定时任务中，对这些orders进行处理，出价最高的，作为竞价成功者，扣除用户的账户余额，去掉对应的保证金，并通知到用户
                //对于竞价失败的，去掉用户账户的对应保证金，
                redisTemplate.opsForSet().add(Constant.OFFER_SET + advertisementId, uId);
            } else {
                ordersMapper.updateByPrimaryKeySelective(orders);
                orders.setuId(uId);
            }
            redisTemplate.opsForHash().put(Constant.USER_SESSION + uId, Constant.DEPOSIT, mysqlDeposit);
            //把当前的出价作为最高价写入广告缓存中，这样便于其他人从缓存中能拿到广告位的当前最高价
            Advertisement updateAdvertisement = new Advertisement();
            updateAdvertisement.setoId(orderId);
            updateAdvertisement.setAmount(offerAmount);
            updateAdvertisement.setUpdated(created);
            redisTemplate.opsForHash().putAll(Constant.ADVERTISEMENT_RECORD + advertisementId, EntityUtils.objectToMap_v1(updateAdvertisement));
            updateAdvertisement.setId(advertisementId);
            advertisementMapper.updateByPrimaryKeySelective(updateAdvertisement);
            redisTemplate.opsForValue().set(Constant.OFFER_RECORD + advertisementId + ":" + uId, orders);//注意，key 是 广告位id 和用户id 的组合
            logger.info("offerToAdvertisement:保证金增加 uId:{},amount:{},deposit:{},ordersId:{},offerCount:{}", uId, deposit, mysqlDeposit, orderId, offerCount);
            JSONObject result = new JSONObject();
            result.put("deposit", mysqlDeposit);
            result.put("offerCount", offerCount);
            return CommonResult.ok(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
            logger.error("orders:出价失败:{}", e.getMessage());
            return CommonResult.build(Constant.UNKNOWN_ERROR, "未知错误");
        } finally {
            redisLock.unlock();
        }
    }


    /**
     * 根据当前广告位的单价和时间段，判断出价是否合法，如果在给定的时间段内，没有高于任何一个历史出价
     * 视为无效出价
     *
     * @param liveRoomId  直播间id
     * @param currentDate 当前时间点
     * @param amount      广告位单价
     * @param startDate   广告位开始时间
     * @param endDate     广告位结束时间
     * @return
     */
    private CommonResult checkOffer(String liveRoomId, Date currentDate, BigDecimal amount, Date startDate, Date endDate) {
        Calendar nowCalendar = Calendar.getInstance();
        int currentMinute = nowCalendar.get(Calendar.MINUTE);
        nowCalendar.add(Calendar.HOUR, 2);
        nowCalendar.set(Calendar.MINUTE, 0);
        nowCalendar.set(Calendar.SECOND, 0);
        nowCalendar.set(Calendar.MILLISECOND, 0);
        Date minEndDate = nowCalendar.getTime();
        if (endDate.before(minEndDate)) {
            logger.error("checkOffer: 广告投放结束时间小于最小投放时段的结束时间，用户投放结束时间：{},最小结束时间点：{}", endDate, minEndDate);
            return CommonResult.build(Constant.PARAMETER_INVALID, "广告投放结束时间小于最小投放时段的结束时间");
        } else if (currentMinute > 59 && endDate.equals(minEndDate)) {
            logger.error("checkOffer: 大于59分钟，禁止投放下一个小时时段的广告，用户投放结束时间：{},最小结束时间点：{}", endDate, minEndDate);
            return CommonResult.build(Constant.PARAMETER_INVALID, "大于59分钟，禁止投放下一个小时时段的广告");
        }
        if (endDate.getTime() - startDate.getTime() < 60 * 60 * 1000) {
            logger.error("checkOffer: 广告投放时间段小于一个小时，开始时间点：{},结束时间：{}", startDate, endDate);
            return CommonResult.build(Constant.PARAMETER_INVALID, "广告投放时间段小于一个小时");
        }


        return null;
    }

    /**
     * //根据广告位到期日期，生成key,缓存进redis ,
     *
     * @param advertisementId
     * @param endDate
     */
    private void saveInExpireSet(String advertisementId, Date endDate) {
        String formatDate = DateUtils.formatDate(endDate, Constant.DATE_HOUR_FORMAT);
        String expireKey = Constant.ADVERTISEMENT_EXPIRE_SET + formatDate;
        redisTemplate.opsForSet().add(expireKey, advertisementId);
    }

    @PostMapping("/queryAdvertisement_v1")
    public CommonResult queryAdvertisement_v1(@RequestBody JSONObject info) {
        String uId = request.getHeader("uId");

        String liveRoomId = info.getString("liveRoomId");

        int page = 1;
        int size = 20;
        if (info.containsKey("page")) {
            page = (int) info.get("page");
        }
        if (info.containsKey("size")) {
            size = (int) info.get("size");
        }
        Advertisement myAdvertisement = null;
        Map maxPriceRecord = null;
        if (page == 1) {
            //当是第一次查询的时候，返回该直播间下未来24小时内的最高价记录，前端一次展示和限制用户出价
            maxPriceRecord = redisTemplate.opsForHash().entries(Constant.MAX_PRICE_RECORD + liveRoomId);
            //如果是第一页，去查询是否自己当前在该直播间是否有没过期的广告
            myAdvertisement = advertisementMapper.selectMyValidAdvertisement(liveRoomId, uId);
        }

        PageHelper.startPage(page, size);
        List<Advertisement> advertisements = advertisementMapper.selectAdvertisementByLiveRoomId(liveRoomId, uId);
        //把自己的历史投放记录插入第一个位置，但是也可能是空的
        if (myAdvertisement != null) {
            myAdvertisement.setuId(uId);
            advertisements.add(0, myAdvertisement);
        }
        PageInfo<Advertisement> advertisementPageInfo = new PageInfo<>(advertisements);
        JSONObject result = new JSONObject();
        if (page == 1 && !maxPriceRecord.isEmpty()) {
            result.put("maxPriceRecord", maxPriceRecord);
        }
        result.put("pageInfo", advertisementPageInfo);
        return CommonResult.ok(result);
    }


    private User getUser(String uId) {
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(Constant.USER_SESSION + uId);
        if(entries.isEmpty()){
            return  userMapper.selectByPrimaryKey(uId);
        }
        return JSONObject.parseObject(JSONObject.toJSONString(entries), User.class);
    }

}
